#include <algorithm>

#include <QByteArray>
#include <QDataStream>
#include <QDebug>
#include <QDate>
#include <QApplication>
#include <QProgressDialog>
#include <QSettings>
#include <QInputDialog>
#include <QFileDialog>
#include <QStandardPaths>
#include <QTemporaryFile>

#include "udfmodel.h"
#include "diutils.h"
#include "dtrangedialog.h"

namespace {
    const int colPos[] = {0, 4, 8, 10, 14, 18, 22, 24};
}

QDataStream& operator >>(QDataStream& is, UDF& udf)
{
    is.setByteOrder(QDataStream::LittleEndian);
    is >> udf.SID >> udf.DID >> udf.MID >> udf.BID >> udf.Res >> udf.JS >> udf.LEN;
    is.skipRawData(udf.LEN);
    return is;
}

UDFModel::UDFModel(const QString& fileName, QObject *parent)
    : ProtocolTableModel(parent), showCount(0), cacheRow(-1)
{
    file.setFileName(fileName);
}

qint64 UDFModel::position(const QModelIndex &index)
{
    int row = index.row();
    int col = index.column();
    qint64 pos = row > 0 ? records.at(row - 1) : 0;
    if (col < 1)
        return pos;
    else
        return pos + colPos[col-1];
}

QModelIndex UDFModel::locate(qint64 pos)
{
    QVector<qint64>::iterator it = std::upper_bound(records.begin(), records.end(), pos);
    int row = std::distance(records.begin(), it);
    qint64 record = row == 0 ? 0 : *(--it);
    pos -= record;
    int col = std::distance(colPos, std::upper_bound(colPos, colPos + 8, pos));

    return index(row, col);
}

bool UDFModel::deleteRows(const QString &fileName, const QModelIndexList &rows)
{
    if (!calcRecords()) {
        return false;
    }

    if (!file.open(QIODevice::ReadOnly))
        return false;
    QFileGuard fg1(file);

    QFile out(fileName);
    if (!out.open(QIODevice::ReadWrite))
        return false;
    QFileGuard fg2(out);

    QProgressDialog progress(tr("File Saving ..."), tr("Cancel"), 0, records.size(), qApp->activeWindow());
    progress.setWindowTitle(tr("Progress"));
    progress.setModal(true);

    QMap<int, QModelIndex> delRows;
    for (QModelIndexList::const_iterator it = rows.begin(); it != rows.end(); it++) {
        QModelIndex index = *it;
        delRows.insert(index.row(), index);
    }
    progress.setMaximum(records.size());
    int step = 0;
    qint64 pos = 0;
    for (QVector<qint64>::iterator it = records.begin(); it != records.end(); it++) {
        qint64 record = *it;
        QByteArray data = file.read(record - pos);
        pos = record;
        if (!delRows.contains(step)) {
            out.write(data);
        }
        progress.setValue(++step);
        qApp->processEvents();
        if (progress.wasCanceled())
            return false;
    }

    return true;
}

bool UDFModel::deleteColumns(const QString &fileName, const QModelIndexList &cols)
{
    if (!calcRecords()) {
        return false;
    }

    if (!file.open(QIODevice::ReadOnly))
        return false;
    QFileGuard fg1(file);

    QFile out(fileName);
    if (!out.open(QIODevice::ReadWrite))
        return false;
    QFileGuard fg2(out);

    QMap<int, QModelIndex> delCols;
    for (QModelIndexList::const_iterator it = cols.begin(); it != cols.end(); it++) {
        int col = it->column();
        if (col)
            delCols.insert(col, *it);
    }

    QProgressDialog progress(tr("File Saving ..."), tr("Cancel"), 0, records.size(), qApp->activeWindow(), Qt::WindowStaysOnTopHint);
    progress.setWindowTitle(tr("Progress"));
    progress.setModal(true);

    int step = 0;
    qint64 pos = 0;
    for (QVector<qint64>::iterator it = records.begin(); it != records.end(); it++) {
        qint64 record = *it;
        QByteArray data = file.read(record - pos);
        pos = record;
        for (int i = 1; i < 8; i++) {
            if (delCols.contains(i))
                continue;
            out.write(data.mid(colPos[i-1], colPos[i] - colPos[i-1]));
        }
        if (!delCols.contains(8)) {
            out.write(data.right(data.size() - colPos[7]));
        }

        progress.setValue(++step);
        qApp->processEvents();
        if (progress.wasCanceled())
            return false;
    }

    return true;
}

bool UDFModel::saveRows(const QString &fileName, const QModelIndexList &rows)
{
    if (!file.open(QIODevice::ReadOnly))
        return false;
    QFileGuard fg1(file);

    QFile out(fileName);
    if (!out.open(QIODevice::ReadWrite))
        return false;
    QFileGuard fg2(out);

    QProgressDialog progress(tr("Save selected rows as..."), tr("Cancel"), 0, rows.size(), qApp->activeWindow());
    progress.setWindowTitle(tr("Progress"));
    progress.setModal(true);
    int step = 0;
    for (QModelIndexList::const_iterator it = rows.begin(); it != rows.end(); it++) {
        QModelIndex index = *it;
        int row = index.row();
        qint64 from = row > 0 ? records.at(row - 1) : 0;
        file.seek(from);
        qint64 size = row > 0 ? records.at(row) - records.at(row - 1) : records.at(row);
        out.write(file.read(size));
        progress.setValue(++step);
        qApp->processEvents();
        if (progress.wasCanceled())
            return false;
    }

    return true;
}

bool UDFModel::saveColumns(const QString &fileName, const QModelIndexList &cols)
{
    if (!calcRecords())
        return false;

    if (!file.open(QIODevice::ReadOnly))
        return false;
    QFileGuard fg1(file);

    QFile out(fileName);
    if (!out.open(QIODevice::ReadWrite))
        return false;
    QFileGuard fg2(out);

    QProgressDialog progress(tr("Save selected columns as..."), tr("Cancel"), 0, records.size(), qApp->activeWindow());
    progress.setWindowTitle(tr("Progress"));
    progress.setModal(true);

    QMap<int, QModelIndex> selectedCols;
    for (QModelIndexList::const_iterator it = cols.begin(); it != cols.end(); it++) {
        int col = it->column();
        if (col)
            selectedCols.insert(col, *it);
    }

    int step = 0;
    qint64 pos = 0;
    for (QVector<qint64>::iterator it = records.begin(); it != records.end(); it++) {
        qint64 record = *it;
        QByteArray data = file.read(record - pos);
        pos = record;
        for (int i = 1; i < 8; i++) {
            if (selectedCols.contains(i))
                out.write(data.mid(colPos[i-1], colPos[i] - colPos[i-1]));
        }
        if (selectedCols.contains(8)) {
            out.write(data.right(data.size() - colPos[7]));
        }

        progress.setValue(++step);
        qApp->processEvents();
        if (progress.wasCanceled())
            return false;
    }

    return true;
}

bool UDFModel::supportFilter(const QModelIndex &index)
{
    switch (index.column()) {
    case 0:
    case 1:
    case 2:
    case 3:
    case 4:
    case 6:
    case 7:
        return true;
    }

    return false;
}

void UDFModel::filter(const QModelIndex &index)
{
    QVector<qint64> records1 =records;
    switch (index.column()) {
    case 0:
        filterSize(index);
        break;
    case 1:
        filterSID(index);
        break;
    case 2:
        filterDID(index);
        break;
    case 3:
        filterMID(index);
        break;
    case 4:
        filterBID(index);
        break;
    case 6:
        filterJS(index);
        break;
    case 7:
        filterLEN(index);
        break;
    }
    records =records1;
}

void UDFModel::loadRecord(int row) const
{
    if (cacheRow == row)
        return;

    qint64 pos = row > 0 ? records.at(row - 1) : 0;
    if (file.open(QIODevice::ReadOnly)) {
        file.seek(pos);
        int size = records.at(row) - pos;
        int displaySize = maxDisplaySize();
        if (size > displaySize)
            size = displaySize;
        record = file.read(size);
        file.close();
        QDataStream in(record);
        in >> udf;
        cacheRow = row;
    }

    if (cacheRow == row)
        return;
}

bool UDFModel::calcRecords()
{
    quint16 len = 0;
    qint64 size = file.size();
    if (size > records.last()) {
        qint64 pos = records.isEmpty() ? 0 : records.last();
        if (file.open(QIODevice::ReadOnly)) {
            QProgressDialog progress(tr("Calculate all records: "), tr("Cancel"), pos, size, qApp->activeWindow());
            progress.setWindowTitle(tr("Progress"));
            progress.setModal(true);

            file.seek(pos);
            QDataStream is(&file);
            is.setByteOrder(QDataStream::LittleEndian);
            while (size - file.pos() >= 22) {
                is.skipRawData(22);
                is >> len;
                is.skipRawData(len);
                records.push_back(file.pos());
                progress.setValue(file.pos());
                qApp->processEvents();
                if (progress.wasCanceled()) {
                    file.close();
                    return false;
                }
            }
            file.close();
        }
    }

    return true;
}

int UDFModel::maxDisplaySize() const
{
    QSettings settings;
    int size = settings.value("UDF/maxDisplaySize", 0).toInt();
    if (0 == size) {
        size = 1024;
        settings.setValue("UDF/maxDisplaySize", size);
    }

    return size;
}

void UDFModel::filterSize(const QModelIndex &index)
{
    QInputDialog input;
    input.setInputMode(QInputDialog::IntInput);
    input.setWindowTitle(tr("Filter"));
    input.setLabelText(tr("Bytes"));
    input.setOkButtonText(tr("OK"));
    input.setCancelButtonText(tr("Cancel"));
    input.setIntMaximum(INT_MAX);
    input.setIntValue(data(index, Qt::DisplayRole).toInt());
    if (!input.exec())
        return;

    qint64 bytes = input.intValue();
    if (bytes <= 0)
        return;

    if (!calcRecords())
        return;

    QFileGuard fg(file, QFile::ReadOnly);
    if (!fg.isOpened())
        return;

    QTemporaryFile out(QDir::temp().filePath(QFileInfo(file).fileName()));
    out.setAutoRemove(false);
    if (out.open()) {
        qint64 from = 0;
        for (QVector<qint64>::const_iterator it = records.cbegin(); it != records.cend(); it++) {
            if (*it - from == bytes) {
                file.seek(from);
                out.write(file.read(bytes));
            }
            from = *it;
        }
        out.close();
        emit openTempFile(out.fileName());
    } 
}

void UDFModel::filterMID(const QModelIndex &index)
{
    QInputDialog input;
    input.setInputMode(QInputDialog::IntInput);
    input.setWindowTitle(tr("Filter"));
    input.setLabelText(tr("MID"));
    input.setOkButtonText(tr("OK"));
    input.setCancelButtonText(tr("Cancel"));
    input.setTextValue(data(index, Qt::DisplayRole).toString());
    if (!input.exec())
        return;

    QByteArray ba = QByteArray::fromHex(input.textValue().toLatin1());
    quint16 mid;
    QDataStream in(ba);
    in.setByteOrder(QDataStream::LittleEndian);
    in >> mid;

    if (!calcRecords())
        return;

    QFileGuard fg(file, QFile::ReadOnly);
    if (!fg.isOpened())
        return;

    QTemporaryFile out(QDir::temp().filePath(QFileInfo(file).fileName()));
    out.setAutoRemove(false);
    if (out.open()) {
        UDF udf;
        qint64 from = 0;
        for (QVector<qint64>::const_iterator it = records.cbegin(); it != records.cend(); from = *it, it++) {
            file.seek(from);
            QByteArray record = file.read(24);
            QDataStream in(record);
            in >> udf;
            if (udf.MID == mid) {
                out.write(record);
                out.write(file.read(udf.LEN));
            }
        }
        out.close();
        emit openTempFile(out.fileName());
    }  
}

void UDFModel::filterSID(const QModelIndex &index)
{
    QInputDialog input;
    input.setInputMode(QInputDialog::IntInput);
    input.setWindowTitle(tr("Filter"));
    input.setLabelText(tr("SID"));
    input.setOkButtonText(tr("OK"));
    input.setCancelButtonText(tr("Cancel"));
    input.setTextValue(data(index, Qt::DisplayRole).toString());
    if (!input.exec())
        return;

    QByteArray ba = QByteArray::fromHex(input.textValue().toLatin1());
    quint32 sid;
    QDataStream in(ba);
    in.setByteOrder(QDataStream::LittleEndian);
    in >> sid;

    if (!calcRecords())
        return;

    QFileGuard fg(file, QFile::ReadOnly);
    if (!fg.isOpened())
        return;

    QTemporaryFile out(QDir::temp().filePath(QFileInfo(file).fileName()));
    out.setAutoRemove(false);
    if (out.open()) {
        UDF udf;
        qint64 from = 0;
        for (QVector<qint64>::const_iterator it = records.cbegin(); it != records.cend(); from = *it, it++) {
            file.seek(from);
            QByteArray record = file.read(24);
            QDataStream in(record);
            in >> udf;
            if (udf.SID == sid) {
                out.write(record);
                out.write(file.read(udf.LEN));
            }
        }
        out.close();
        emit openTempFile(out.fileName());
    }
}

void UDFModel::filterDID(const QModelIndex &index)
{
    QInputDialog input;
    input.setInputMode(QInputDialog::IntInput);
    input.setWindowTitle(tr("Filter"));
    input.setLabelText(tr("DID"));
    input.setOkButtonText(tr("OK"));
    input.setCancelButtonText(tr("Cancel"));
    input.setTextValue(data(index, Qt::DisplayRole).toString());
    if (!input.exec())
        return;

    QByteArray ba = QByteArray::fromHex(input.textValue().toLatin1());
    quint32 did;
    QDataStream in(ba);
    in.setByteOrder(QDataStream::LittleEndian);
    in >> did;

    if (!calcRecords())
        return;

    QFileGuard fg(file, QFile::ReadOnly);
    if (!fg.isOpened())
        return;

    QTemporaryFile out(QDir::temp().filePath(QFileInfo(file).fileName()));
    out.setAutoRemove(false);
    QFileGuard tmp(out);
    if (out.open()) {
        UDF udf;
        qint64 from = 0;
        for (QVector<qint64>::const_iterator it = records.cbegin(); it != records.cend(); from = *it, it++) {
            file.seek(from);
            QByteArray record = file.read(24);
            QDataStream in(record);
            in >> udf;
            if (udf.DID == did) {
                out.write(record);
                out.write(file.read(udf.LEN));
            }
        }
        emit openTempFile(out.fileName());
    }
}

void UDFModel::filterBID(const QModelIndex &index)
{
    QInputDialog input;
    input.setInputMode(QInputDialog::IntInput);
    input.setWindowTitle(tr("Filter"));
    input.setLabelText(tr("BID"));
    input.setOkButtonText(tr("OK"));
    input.setCancelButtonText(tr("Cancel"));
    input.setTextValue(data(index, Qt::DisplayRole).toString());
    if (!input.exec())
        return;

    QByteArray ba = QByteArray::fromHex(input.textValue().toLatin1());
    quint32 bid;
    QDataStream in(ba);
    in.setByteOrder(QDataStream::LittleEndian);
    in >> bid;

    if (!calcRecords())
        return;

    QFileGuard fg(file, QFile::ReadOnly);
    if (!fg.isOpened())
        return;

    QTemporaryFile out(QDir::temp().filePath(QFileInfo(file).fileName()));
    out.setAutoRemove(false);
    if (out.open()) {
        UDF udf;
        qint64 from = 0;
        for (QVector<qint64>::const_iterator it = records.cbegin(); it != records.cend(); from = *it, it++) {
            file.seek(from);
            QByteArray record = file.read(24);
            QDataStream in(record);
            in >> udf;
            if (udf.BID == bid) {
                out.write(record);
                out.write(file.read(udf.LEN));
            }
        }
        out.close();
        emit openTempFile(out.fileName());
    }
}

void UDFModel::filterJS(const QModelIndex &index)
{
    loadRecord(index.row());

    DTRangeDialog dlg(qApp->activeWindow());
    dlg.disableDate();
    dlg.setStartTime(udf.time());
    dlg.setEndTime(udf.time());
    if (!dlg.exec())
        return;

    if (!calcRecords())
        return;

    QFileGuard fg(file, QFile::ReadOnly);
    if (!fg.isOpened())
        return;

    QTemporaryFile out(QDir::temp().filePath(QFileInfo(file).fileName()));
    out.setAutoRemove(false);
    if (out.open()) {
        UDF udf;
        qint64 from = 0;
        if (dlg.hasBegin() && !dlg.hasEnd()) {
            QTime startTime = dlg.startTime();
            for (QVector<qint64>::const_iterator it = records.cbegin(); it != records.cend(); from = *it, it++) {
                file.seek(from);
                QByteArray record = file.read(24);
                QDataStream in(record);
                in >> udf;
                QTime time = udf.time();
                if (time >= startTime) {
                    out.write(record);
                    out.write(file.read(udf.LEN));
                }
            }
        } else if (dlg.hasBegin() && dlg.hasEnd()) {
            QTime startTime = dlg.startTime();
            QTime endTime = dlg.endTime();
            qint64 from = 0;
            for (QVector<qint64>::const_iterator it = records.cbegin(); it != records.cend(); from = *it, it++) {
                file.seek(from);
                QByteArray record = file.read(24);
                QDataStream in(record);
                in >> udf;
                QTime time = udf.time();
                if (time >= startTime && time <= endTime) {
                    out.write(record);
                    out.write(file.read(udf.LEN));
                }
            }
        } else if (!dlg.hasBegin() && dlg.hasEnd()) {
            QTime endTime = dlg.endTime();
            qint64 from = 0;
            for (QVector<qint64>::const_iterator it = records.cbegin(); it != records.cend(); from = *it, it++) {
                file.seek(from);
                QByteArray record = file.read(24);
                QDataStream in(record);
                in >> udf;
                QTime time = udf.time();
                if (time <= endTime) {
                    out.write(record);
                    out.write(file.read(udf.LEN));
                }
            }
        }
        out.close();
        emit openTempFile(out.fileName());
    }
}

void UDFModel::filterLEN(const QModelIndex &index)
{
    QInputDialog input;
    input.setInputMode(QInputDialog::IntInput);
    input.setWindowTitle(tr("Filter"));
    input.setLabelText(tr("LEN"));
    input.setOkButtonText(tr("OK"));
    input.setCancelButtonText(tr("Cancel"));
    input.setTextValue(data(index, Qt::DisplayRole).toString());
    if (!input.exec())
        return;

    QByteArray ba = QByteArray::fromHex(input.textValue().toLatin1());
    quint16 len;
    QDataStream in(ba);
    in.setByteOrder(QDataStream::LittleEndian);
    in >> len;
    if (len <= 0)
        return;

    qint64 bytes = len + 24;

    if (!calcRecords())
        return;

    QFileGuard fg(file, QFile::ReadOnly);
    if (!fg.isOpened())
        return;

    QTemporaryFile out(QDir::temp().filePath(QFileInfo(file).fileName()));
    out.setAutoRemove(false);
    if (out.open()) {
        qint64 from = 0;
        for (QVector<qint64>::const_iterator it = records.cbegin(); it != records.cend(); from = *it, it++) {
            file.seek(from);
            if (*it - from == bytes) {
                out.write(file.read(bytes));
            }
        }
        out.close();
        emit openTempFile(out.fileName());
    }
}

int UDFModel::rowCount(const QModelIndex &parent) const
{
    return parent.isValid() ? 0 : showCount;
}

int UDFModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);

    return 9;
}

QVariant UDFModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    int row = index.row();
    if (row >= records.size() || row < 0)
        return QVariant();

    if (role == Qt::DisplayRole) {
        loadRecord(row);
        int recordSize = 24 + udf.LEN;
        int col = index.column();
        if (0 == col) {
            return recordSize;
        } else if (col > 0 && col < 8) {
            return toHex(record.mid(colPos[col-1], colPos[col] - colPos[col-1]), ' ');
        } else if (8 == col) {
            QString cell = toHex(record.mid(colPos[7]), ' ');
            if (recordSize > record.size())
                cell.append(" ...");

            return cell;
        }
    }

    return QVariant();
}

void UDFModel::fetchMore(const QModelIndex &parent)
{
    if (parent.isValid())
        return;

    int count = 0;

    if (showCount < records.size()) {
        count = records.size() - showCount;
        if (count > 100)
            count = 100;
    } else {
        quint16 len = 0;
        qint64 pos = records.isEmpty() ? 0 : records.last();
        if (file.open(QIODevice::ReadOnly)) {
            file.seek(pos);
            QDataStream is(&file);
            is.setByteOrder(QDataStream::LittleEndian);
            qint64 size = file.size();
            while (size - file.pos() >= 24) {
                is.skipRawData(22);
                is >> len;
                is.skipRawData(len);
                records.push_back(file.pos());
                if (++count >= 100)
                    break;
            }
            file.close();
        }
    }

    if (count <= 0)
        return;

    beginInsertRows(QModelIndex(), showCount, showCount + count - 1);
    showCount += count;
    endInsertRows();
}

bool UDFModel::canFetchMore(const QModelIndex &parent) const
{
    if (parent.isValid())
        return false;

    qint64 pos = records.isEmpty() ? 0 : records.last();
    return (file.size() - pos) >= 24 || showCount < records.size();
}

QVariant UDFModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    static QString headers[] = {
        tr("Bytes"), tr("SID"), tr("DID"), tr("MID"), tr("BID"),
        tr("Res"), tr("JS"), tr("LEN"), tr("DATA"),
    };

    if (role == Qt::DisplayRole) {
        if (orientation == Qt::Horizontal)
            return headers[section];
        else
            return section + 1;
    }

    return QVariant();
}

QTime UDF::time()
{
    QTime time(0, 0);
    return time.addMSecs(JS / 10);
}
